Fix duplicate OAuth code exchange causing invalid_grant crashes (MAILSPRING-CLIENT-37) - #2792
Fix duplicate OAuth code exchange causing invalid_grant crashes (MAILSPRING-CLIENT-37)#2792bengotow wants to merge 2 commits into
Conversation
…LSPRING-CLIENT-37) The local callback server in OAuthSignInPage re-invoked _onReceivedCode() for every request that carried a `code` query param, with no check that the code had already been submitted. Browsers/security software can hit the callback URL more than once for the same redirect (retries, link prefetching, back/forward replay), which resends an authorization code that's already been exchanged. Since auth codes are single-use, the second exchange attempt fails with invalid_grant (AADSTS9002313 on Microsoft, and the equivalent on other providers), which is what was reaching Sentry. Add a _codeReceived guard so only the first code received triggers _onReceivedCode(); the server still returns the redirect response for any later hits so the browser doesn't show an error.
|
Warning Indent Zero is shutting down on August 7th. Please migrate over to Indent 2.0 to continue getting PR reviews.
|
|
Warning This organization's free trial has ended, so Indent couldn't start this review. Manage billing to resume reviews on this repository. |
|
@claude can you make sure that this resets if the user goes back, then forward again in the auth flow, triggering a different code callback? Maybe the state variable could store the last code received so we can accept a new/different code, if this is a possible scenario. |
Per review feedback: the previous guard used a one-shot boolean, which would incorrectly block a legitimate second code if the user goes back in their browser and completes sign-in again with a different account after an error (the server keeps listening as long as the component is mounted, which includes the 'error' authStage while waiting for the user to click "Try Again"). Track the last code received instead, so an identical repeat is still ignored (the original invalid_grant fix) but a genuinely different code is accepted — unless we're already mid- exchange or have already succeeded for a prior code.
|
Good catch — that scenario is real. The server keeps listening for as long as the component is mounted, which includes the Pushed a fix in bf998bc: swapped the boolean for Generated by Claude Code |
|
The That's an infrastructure hiccup unrelated to the diff. I don't have permission to re-run the workflow from here (got a 403), so it'll need a maintainer to re-run the job, or a new push will retrigger it. Generated by Claude Code |
Summary
Fixes MAILSPRING-CLIENT-37 — 111 events / 51 users, unresolved.
Sentry error:
Stacktrace:
oauth-signin-page.js:100 _onReceivedCode → onboarding-helpers.js:210 buildMicrosoftAccountFromAuthResponse → onboarding-helpers.js:44 fetchPostWithFormBodyRoot cause
OAuthSignInPage(used for Gmail, Office 365, and Outlook sign-in) starts a localhttp.createServerincomponentDidMountto receive the OAuth redirect. Its request handler calledthis._onReceivedCode(code)for every incoming request that carried acodequery parameter, with no check for whether a code had already been received and processed:OAuth authorization codes are single-use.
_onReceivedCodetriggers a code exchange that can take several seconds (token exchange + Graph profile fetch + IMAP/SMTP connection test infinalizeAndValidateAccount), and during that window a second request to the same callback URL is plausible — a browser retry, a security product prefetching/scanning the redirect link, or a back/forward replay of history. When that happens, the secondfetchPostWithFormBodycall resends the already-consumed code, and Microsoft's identity platform rejects it withinvalid_grant/AADSTS9002313("Invalid request. Request is malformed or invalid"), which is exactly the error reaching Sentry. The same code path is shared by Gmail and Outlook, so the same failure mode applies there too (surfacing as the provider-specific equivalent of a reused-code error).I verified there's no other issue at play here: the
redirect_urisent during the initial auth request and the token exchange match exactly, and the PKCEcode_verifier/code_challengepair is generated once and stays consistent between the two requests. The systemic, repeated nature of the error (111 occurrences across 51 distinct users) also fits a structural double-invocation bug better than users simply being slow to complete the flow.Fix
Add a
_codeReceivedguard so only the first request carrying a code triggers_onReceivedCode(). Later requests (e.g. a duplicate hit) still get the same 302 redirect response, so the browser experience is unaffected — we just stop re-submitting an already-consumed code to the provider.Test plan
oauth-signin-page.tsx,onboarding-helpers.ts) to confirmredirect_uriand PKCEcode_verifier/code_challengeare consistent between the auth request and token exchange, ruling those out as the cause.OAuthSignInPageis shared by Gmail, Office 365, and Outlook sign-in (page-account-settings-gmail.tsx,page-account-settings-o365.tsx,page-account-settings-outlook.tsx), so the fix covers all three.🤖 Generated with Claude Code
Generated by Claude Code